/*
*    l3qd - Light, light, lightweight queue
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package add

import (
	"bufio"
	"errors"
	"fmt"
	"github.com/spf13/cobra"
	"notabug.org/marcux/l3q/internal/client"
	"notabug.org/marcux/l3q/internal/common"
	"os"
	"os/user"
	"strconv"
	"strings"
)

var addSeqCmdFlagVerbose, addSeqCmdFlagJobid bool
var addSeqCmdFlagJobname, addSeqCmdFlagDepend string
var addSeqCmdFlagCores int
var coresArgInFile, dependArgInFile, jobnameArgInFile bool

var addSeqCmd = &cobra.Command{
	Use:   "seq TASKFILE",
	Args:  cobra.ExactArgs(1),
	Short: "Adds a sequential job of tasks to l3q.",
	Long: `Description:
  Adds a new sequential job of tasks to l3q.
  All tasks will be executed one after another.

  Positional argument TASKFILE is required.
  TASKFILE is path to file containing all tasks that should be run in sequence.
  One task per line, empty lines and lines starting with # are ignored. 
  Lines containing tasks must have one or two column, first column
  specifies a path to an executable program, no arguments can be used for 
  this program, and the second optional column contains working directory. 
  Columns are separated by whitespace. 
  If second column, workdir, is not specified,
  users home directory will be used as default working directory.

  If TASKFILE contains a line with the special syntax specifying 
  arguments these arguments will be used.
  If TASKFILE contains the special syntax line and argument is 
  given on commandline this options will be used and the same options
  in special syntax line will be ignored.
  If TASKFILE contains multiple special syntax lines the last line will be used.

  Special syntax in TASKFILE:
  #--add-seq --jobname The-name-of-the-job --depend 1,3,5`,
	Run: func(cmd *cobra.Command, args []string) {
		job := common.Job{
			Type:       common.JobTypeSequence,
			CoresAlloc: 1,
		}

		if addSeqCmdFlagCores < 0 {
			fmt.Fprintf(os.Stderr, "Argument --cores is required to be a positive integer.\nTry -h or --help for help.\n")
			os.Exit(1)
		} else if addSeqCmdFlagCores > 0 {
			job.CoresAlloc = addSeqCmdFlagCores
		}

		if len(addSeqCmdFlagDepend) > 0 {
			depStrs := strings.Split(strings.TrimSpace(addSeqCmdFlagDepend), ",")

			error := false
			for _, d := range depStrs {
				if _, err := strconv.Atoi(d); err != nil {
					fmt.Fprintf(os.Stderr, "Failed to parse: %s as integer.\n", d)
					error = true
				}
			}

			if error {
				fmt.Fprintf(os.Stderr, "Argument --depend is required to be a comma separated list of integers\n")
				fmt.Fprintf(os.Stderr, "Try -h or --help for help.\n")

				os.Exit(1)
			}

			job.Depend = strings.TrimSpace(addSeqCmdFlagDepend)
		}

		if len(addSeqCmdFlagJobname) > 0 {
			job.Name = addSeqCmdFlagJobname
		}

		usr, err := user.Current()
		if err != nil {
			if addSeqCmdFlagVerbose {
				fmt.Fprintf(os.Stderr, "Failed to get current user.\n%s\n", err)
			} else {
				fmt.Fprintf(os.Stderr, "Failed to get current user.\n")
			}

			os.Exit(1)
		}

		job.User = usr.Username

		grp, err := user.LookupGroupId(usr.Gid)
		if err != nil {
			if addSeqCmdFlagVerbose {
				fmt.Fprintf(os.Stderr, "Failed to lookup current users main group.\n%s\n", err)
			} else {
				fmt.Fprintf(os.Stderr, "Failed to lookup current users masin group.\n")
			}

			os.Exit(1)
		}

		job.Group = grp.Name

		err = parseTaskFile(&job, args[0])
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to parse TASKFILE: \n%s\n", err)
			os.Exit(1)
		}

		if coresArgInFile {
			if addSeqCmdFlagCores > 0 {
				job.CoresAlloc = addSeqCmdFlagCores
			}
		}

		if dependArgInFile {
			if len(addSeqCmdFlagDepend) > 0 {
				job.Depend = strings.TrimSpace(addSeqCmdFlagDepend)
			}
		}

		if jobnameArgInFile {
			if len(addSeqCmdFlagJobname) > 0 {
				job.Name = addSeqCmdFlagJobname
			}
		}

		job.HostsAlloc = 1

		if len(job.Name) == 0 {
			job.Name = client.GenerateJobName()
		}

		id := client.UrlPostJob("/job", addSeqCmdFlagVerbose, job)

		if addSeqCmdFlagJobid {
			fmt.Printf("%d\n", id)
		} else {
			fmt.Printf("Job with name: %s and Id: %d, sussessfully added.\n", job.Name, id)
		}
	},
}

func parseFileArgs(job *common.Job, argLine string) error {
	if !strings.HasPrefix(argLine, "#--add-seq ") {
		return fmt.Errorf("Line does not start with required: #--add-seq")
	}

	argLine, _ = strings.CutPrefix(argLine, "#--add-seq ")
	args := strings.Fields(argLine)

	errStr := ""
	nextArg := true
	arg := ""
	for _, a := range args {
		if nextArg {
			switch a {
			case "-c", "--cores":
				arg = "--cores"
				nextArg = false
			case "-d", "--depend":
				arg = "--depend"
				nextArg = false
			case "-j", "--jobname":
				arg = "--jobname"
				nextArg = false
			case "-i", "--jobid":
				addSeqCmdFlagJobid = true
				nextArg = true
			default:
				return fmt.Errorf("Wrong argument, argument: %s, unknown", a)
			}
		} else {
			switch arg {
			case "--cores":
				c, err := strconv.Atoi(a)
				if err != nil {
					errStr = fmt.Sprintf("%s\nFailed to parse argument --cores: %s, %s",
						errStr, a, err)
				} else if c <= 0 {
					errStr = fmt.Sprintf("%s\nArgument --cores: %s, is not a valid positiv integer.",
						errStr, a)
				} else {
					job.CoresAlloc = c
					coresArgInFile = true
				}
			case "--depend":
				depStrs := strings.Split(strings.TrimSpace(a), ",")

				error := false
				for _, d := range depStrs {
					if _, err := strconv.Atoi(d); err != nil {
						errStr = fmt.Sprintf("%s\nFailed to parse depend value: %s as integer.",
							errStr, d)
						error = true
					}
				}

				if error {
					errStr = fmt.Sprintf("%s\nArgument --depend: %s, is required to be a comma separated list of positive integers.", errStr, a)
				} else {
					job.Depend = strings.TrimSpace(a)
					dependArgInFile = true
				}
			case "--jobname":
				job.Name = a
				jobnameArgInFile = true
			}

			nextArg = true
		}
	}

	if len(errStr) > 0 {
		return fmt.Errorf("%s", errStr)
	}

	return nil
}

func parseTaskFile(job *common.Job, filePath string) error {
	file, err := os.Open(filePath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("File: %s can not be found.", filePath)
		} else {
			return fmt.Errorf("Failed to open file: %s, %s", filePath, err)
		}
	}

	defer file.Close()

	scanner := bufio.NewScanner(file)

	i := 0
	seqno := 0
	argLine := ""
	lineErr := ""
	for scanner.Scan() {
		i++
		line := scanner.Text()

		if strings.HasPrefix(line, "#--add-seq ") {
			argLine = line
		} else if strings.HasPrefix(line, "#") {
			// comment
			continue
		} else if len(strings.TrimSpace(line)) == 0 {
			// empty line
			continue
		} else {
			task := common.Task{}
			// parse task
			fields := strings.Fields(line)
			if len(fields) > 2 {
				lineErr = fmt.Sprintf("%sLine: %d Too many fields in line. Max fields: %d, fields in line: %d\n",
					lineErr, i, 2, len(fields))
				continue
			}

			seqno++
			task.Seqno = seqno
			task.Command = fields[0]

			if len(fields) == 1 {
				task.WorkDir = "$HOME"
			} else {
				task.WorkDir = fields[1]
			}

			job.Tasks = append(job.Tasks, task)
		}
	}

	if len(lineErr) > 0 {
		return fmt.Errorf("Error in %s:\n%s", filePath, lineErr)
	}

	if len(argLine) > 0 {
		err := parseFileArgs(job, argLine)
		if err != nil {
			return fmt.Errorf("Error in %s:\nFailed to parse argument line:\n%s\n%s",
				filePath, argLine, err)
		}
	}

	return nil
}

func init() {
	addSeqCmd.Flags().BoolVarP(&addSeqCmdFlagVerbose, "verbose", "v", false, "Print verbose output")
	addSeqCmd.Flags().BoolVarP(&addSeqCmdFlagJobid, "jobid", "i", false, "Jobid of this job will be returned on success\nas the only output.\nOn failure the error message is printed.")
	addSeqCmd.Flags().StringVarP(&addSeqCmdFlagJobname, "jobname", "j", "", "Name of specified job")
	addSeqCmd.Flags().StringVarP(&addSeqCmdFlagDepend, "depend", "d", "",
		`Comma separated list of other jobid that this job depends on.
All jobs specified in this joblist have to terminate without 
errors before this job is started. If a depenedent job fails 
this job will also fail and will not be started.
Example: 1,5,9 (comma separated without whitespaces)`)
	addSeqCmd.Flags().IntVarP(&addSeqCmdFlagCores, "cores", "c", 0,
		`Number of cores to use for each task.
If flag is not specified, default value 1 is used.`)
}
